/*
 * Copyright (C) 2018 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include <stdlib.h>
#include <string.h>
#ifdef HAVE_GIO_UNIX
#include <glib-unix.h>
#endif
#include <gio/gio.h>
#include <locale.h>

#include "xb-builder.h"
#include "xb-node.h"
#include "xb-silo-export.h"
#include "xb-silo-query.h"

typedef struct {
	GCancellable *cancellable;
	GMainLoop *loop;
	GPtrArray *cmd_array;
	gboolean force;
	gboolean wait;
	gboolean profile;
	gchar **tokenize;
} XbToolPrivate;

static void
xb_tool_private_free(XbToolPrivate *priv)
{
	if (priv == NULL)
		return;
	if (priv->cmd_array != NULL)
		g_ptr_array_unref(priv->cmd_array);
	g_main_loop_unref(priv->loop);
	g_object_unref(priv->cancellable);
	g_strfreev(priv->tokenize);
	g_free(priv);
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
G_DEFINE_AUTOPTR_CLEANUP_FUNC(XbToolPrivate, xb_tool_private_free)
#pragma clang diagnostic pop

typedef gboolean (*FuUtilPrivateCb)(XbToolPrivate *util, gchar **values, GError **error);

typedef struct {
	gchar *name;
	gchar *arguments;
	gchar *description;
	FuUtilPrivateCb callback;
} FuUtilItem;

static void
xb_tool_item_free(FuUtilItem *item)
{
	g_free(item->name);
	g_free(item->arguments);
	g_free(item->description);
	g_free(item);
}

static gint
xb_tool_sort_command_name_cb(FuUtilItem **item1, FuUtilItem **item2)
{
	return g_strcmp0((*item1)->name, (*item2)->name);
}

static void
xb_tool_add(GPtrArray *array,
	    const gchar *name,
	    const gchar *arguments,
	    const gchar *description,
	    FuUtilPrivateCb callback)
{
	g_auto(GStrv) names = NULL;

	g_return_if_fail(name != NULL);
	g_return_if_fail(description != NULL);
	g_return_if_fail(callback != NULL);

	/* add each one */
	names = g_strsplit(name, ",", -1);
	for (guint i = 0; names[i] != NULL; i++) {
		FuUtilItem *item = g_new0(FuUtilItem, 1);
		item->name = g_strdup(names[i]);
		if (i == 0) {
			item->description = g_strdup(description);
		} else {
			/* TRANSLATORS: this is a command alias, e.g. 'get-devices' */
			item->description = g_strdup_printf("Alias to %s", names[0]);
		}
		item->arguments = g_strdup(arguments);
		item->callback = callback;
		g_ptr_array_add(array, item);
	}
}

static void
xb_tool_cancelled_cb(GCancellable *cancellable, gpointer user_data)
{
	XbToolPrivate *priv = (XbToolPrivate *)user_data;
	g_print("Cancelled!\n");
	g_main_loop_quit(priv->loop);
}

static gchar *
xb_tool_get_descriptions(GPtrArray *array)
{
	gsize len;
	const gsize max_len = 31;
	FuUtilItem *item;
	GString *string;

	/* print each command */
	string = g_string_new("");
	for (guint i = 0; i < array->len; i++) {
		item = g_ptr_array_index(array, i);
		g_string_append(string, "  ");
		g_string_append(string, item->name);
		len = strlen(item->name) + 2;
		if (item->arguments != NULL) {
			g_string_append(string, " ");
			g_string_append(string, item->arguments);
			len += strlen(item->arguments) + 1;
		}
		if (len < max_len) {
			for (guint j = len; j < max_len + 1; j++)
				g_string_append_c(string, ' ');
			g_string_append(string, item->description);
			g_string_append_c(string, '\n');
		} else {
			g_string_append_c(string, '\n');
			for (guint j = 0; j < max_len + 1; j++)
				g_string_append_c(string, ' ');
			g_string_append(string, item->description);
			g_string_append_c(string, '\n');
		}
	}

	/* remove trailing newline */
	if (string->len > 0)
		g_string_set_size(string, string->len - 1);

	return g_string_free(string, FALSE);
}

static gboolean
xb_tool_run(XbToolPrivate *priv, const gchar *command, gchar **values, GError **error)
{
	/* find command */
	for (guint i = 0; i < priv->cmd_array->len; i++) {
		FuUtilItem *item = g_ptr_array_index(priv->cmd_array, i);
		if (g_strcmp0(item->name, command) == 0)
			return item->callback(priv, values, error);
	}

	/* not found */
	g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Command not found");
	return FALSE;
}

static gboolean
xb_tool_dump(XbToolPrivate *priv, gchar **values, GError **error)
{
	XbSiloLoadFlags flags = XB_SILO_LOAD_FLAG_NONE;

	/* check args */
	if (g_strv_length(values) < 1) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_FAILED,
				    "Invalid arguments, expected "
				    "FILENAME"
				    " -- e.g. `example.xmlb`");
		return FALSE;
	}

	/* don't check the magic to make fuzzing easier */
	if (priv->force)
		flags |= XB_SILO_LOAD_FLAG_NO_MAGIC;

	/* load blobs */
	for (guint i = 0; values[i] != NULL; i++) {
		g_autofree gchar *str = NULL;
		g_autoptr(GFile) file = g_file_new_for_path(values[i]);
		g_autoptr(XbSilo) silo = xb_silo_new();
		if (!xb_silo_load_from_file(silo, file, flags, NULL, error))
			return FALSE;
		str = xb_silo_to_string(silo, error);
		if (str == NULL)
			return FALSE;
		g_print("%s", str);
	}
	return TRUE;
}

static gboolean
xb_tool_export(XbToolPrivate *priv, gchar **values, GError **error)
{
	XbSiloLoadFlags flags = XB_SILO_LOAD_FLAG_NONE;

	/* check args */
	if (g_strv_length(values) < 1) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_FAILED,
				    "Invalid arguments, expected "
				    "FILENAME"
				    " -- e.g. `example.xmlb`");
		return FALSE;
	}

	/* don't check the magic to make fuzzing easier */
	if (priv->force)
		flags |= XB_SILO_LOAD_FLAG_NO_MAGIC;

	/* load blobs */
	for (guint i = 0; values[i] != NULL; i++) {
		g_autofree gchar *str = NULL;
		g_autoptr(GFile) file = g_file_new_for_path(values[i]);
		g_autoptr(XbSilo) silo = xb_silo_new();
		if (!xb_silo_load_from_file(silo, file, flags, NULL, error))
			return FALSE;
		str = xb_silo_export(
		    silo,
		    XB_NODE_EXPORT_FLAG_ADD_HEADER | XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE |
			XB_NODE_EXPORT_FLAG_FORMAT_INDENT | XB_NODE_EXPORT_FLAG_INCLUDE_SIBLINGS,
		    error);
		if (str == NULL)
			return FALSE;
		g_print("%s", str);
	}
	return TRUE;
}

static gboolean
xb_tool_query(XbToolPrivate *priv, gchar **values, GError **error)
{
	guint limit = 0;
	g_autoptr(GFile) file = NULL;
	g_autoptr(GPtrArray) results = NULL;
	g_autoptr(XbQuery) query = NULL;
	g_autoptr(XbSilo) silo = xb_silo_new();
	g_auto(XbQueryContext) context = XB_QUERY_CONTEXT_INIT();

	/* check args */
	if (g_strv_length(values) < 2) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_FAILED,
				    "Invalid arguments, expected "
				    "FILENAME QUERY [LIMIT]"
				    " -- e.g. `example.xmlb`");
		return FALSE;
	}

	/* load blob */
	file = g_file_new_for_path(values[0]);
	if (priv->profile) {
		xb_silo_set_profile_flags(silo,
					  XB_SILO_PROFILE_FLAG_OPTIMIZER |
					      XB_SILO_PROFILE_FLAG_XPATH |
					      XB_SILO_PROFILE_FLAG_APPEND);
	}
	if (!xb_silo_load_from_file(silo, file, XB_SILO_LOAD_FLAG_NONE, NULL, error))
		return FALSE;

	/* parse optional limit */
	if (g_strv_length(values) == 3)
		limit = g_ascii_strtoull(values[2], NULL, 10);
	xb_query_context_set_limit(&context, limit);

	/* query */
	query = xb_query_new_full(silo, values[1], XB_QUERY_FLAG_OPTIMIZE, error);
	if (query == NULL)
		return FALSE;
	results = xb_silo_query_with_context(silo, query, &context, error);
	if (results == NULL)
		return FALSE;
	for (guint i = 0; i < results->len; i++) {
		XbNode *n = g_ptr_array_index(results, i);
		g_autofree gchar *xml = NULL;
		xml = xb_node_export(n,
				     XB_NODE_EXPORT_FLAG_FORMAT_MULTILINE |
					 XB_NODE_EXPORT_FLAG_FORMAT_INDENT,
				     error);
		if (xml == NULL)
			return FALSE;
		g_print("RESULT: %s\n", xml);
	}

	/* profile */
	if (priv->profile)
		g_print("%s", xb_silo_get_profile_string(silo));

	return TRUE;
}

static gboolean
xb_tool_query_file(XbToolPrivate *priv, gchar **values, GError **error)
{
	g_autoptr(GFile) file = NULL;
	g_autoptr(XbSilo) silo = xb_silo_new();

	/* check args */
	if (g_strv_length(values) < 2) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_FAILED,
				    "Invalid arguments, expected "
				    "FILENAME FILENAME");
		return FALSE;
	}

	/* load blob */
	file = g_file_new_for_path(values[0]);
	if (!xb_silo_load_from_file(silo, file, XB_SILO_LOAD_FLAG_NONE, NULL, error))
		return FALSE;

	/* optionally load file */
	for (guint i = 1; values[i] != NULL; i++) {
		g_autofree gchar *xpath = NULL;
		g_autoptr(GPtrArray) results = NULL;
		g_autoptr(GError) error_local = NULL;

		/* load XPath from file */
		if (!g_file_get_contents(values[i], &xpath, NULL, error))
			return FALSE;
		g_strdelimit(xpath, "\n", '\0');

		/* query */
		results = xb_silo_query(silo, xpath, 0, &error_local);
		if (results == NULL) {
			g_print("FAILED: %s\n", error_local->message);
			continue;
		}
		for (guint j = 0; j < results->len; j++) {
			XbNode *n = g_ptr_array_index(results, j);
			g_autofree gchar *xml = NULL;
			xml = xb_node_export(n, XB_NODE_EXPORT_FLAG_NONE, error);
			if (xml == NULL)
				return FALSE;
			g_print("RESULT: %s\n", xml);
		}
	}

	/* profile */
	if (priv->profile)
		g_print("%s", xb_silo_get_profile_string(silo));

	return TRUE;
}

static void
xb_tool_silo_invalidated_cb(XbSilo *silo, GParamSpec *pspec, gpointer user_data)
{
	XbToolPrivate *priv = (XbToolPrivate *)user_data;
	g_main_loop_quit(priv->loop);
}

static gboolean
xb_tool_builder_tokenize_cb(XbBuilderFixup *self,
			    XbBuilderNode *bn,
			    gpointer user_data,
			    GError **error)
{
	XbToolPrivate *priv = (XbToolPrivate *)user_data;
	for (guint i = 0; priv->tokenize != NULL && priv->tokenize[i] != NULL; i++) {
		if (g_strcmp0(xb_builder_node_get_element(bn), priv->tokenize[i]) == 0) {
			xb_builder_node_tokenize_text(bn);
			break;
		}
	}
	return TRUE;
}

static gboolean
xb_tool_compile(XbToolPrivate *priv, gchar **values, GError **error)
{
	const gchar *const *locales = g_get_language_names();
	g_autoptr(XbBuilder) builder = xb_builder_new();
	g_autoptr(XbSilo) silo = NULL;
	g_autoptr(GFile) file_dst = NULL;

	/* check args */
	if (g_strv_length(values) < 2) {
		g_set_error_literal(error,
				    G_IO_ERROR,
				    G_IO_ERROR_FAILED,
				    "Invalid arguments, expected "
				    "FILE-OUT FILE [FILE]"
				    " -- e.g. `example.xmlb example.xml`");
		return FALSE;
	}

	/* load file */
	for (guint i = 0; locales[i] != NULL; i++)
		xb_builder_add_locale(builder, locales[i]);

	for (guint i = 1; values[i] != NULL; i++) {
		g_autoptr(GFile) file = g_file_new_for_path(values[i]);
		g_autoptr(XbBuilderSource) source = xb_builder_source_new();
		if (priv->tokenize != NULL) {
			g_autoptr(XbBuilderFixup) fixup = NULL;
			fixup = xb_builder_fixup_new("TextTokenize",
						     xb_tool_builder_tokenize_cb,
						     priv,
						     NULL);
			xb_builder_source_add_fixup(source, fixup);
		}
		if (!xb_builder_source_load_file(source,
						 file,
						 XB_BUILDER_SOURCE_FLAG_WATCH_FILE |
						     XB_BUILDER_SOURCE_FLAG_LITERAL_TEXT,
						 NULL,
						 error))
			return FALSE;
		xb_builder_import_source(builder, source);
	}
	file_dst = g_file_new_for_path(values[0]);
	xb_builder_set_profile_flags(builder,
				     priv->profile ? XB_SILO_PROFILE_FLAG_APPEND
						   : XB_SILO_PROFILE_FLAG_NONE);
	silo = xb_builder_ensure(builder,
				 file_dst,
				 XB_BUILDER_COMPILE_FLAG_WATCH_BLOB |
				     XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID |
				     XB_BUILDER_COMPILE_FLAG_NATIVE_LANGS,
				 NULL,
				 error);
	if (silo == NULL)
		return FALSE;

	/* wait for invalidation */
	if (priv->wait) {
		g_print("Waiting for invalidation…\n");
		g_signal_connect(silo,
				 "notify::valid",
				 G_CALLBACK(xb_tool_silo_invalidated_cb),
				 priv);
		g_main_loop_run(priv->loop);
	}

	/* profile */
	if (priv->profile)
		g_print("%s", xb_silo_get_profile_string(silo));

	/* success */
	return TRUE;
}

#ifdef HAVE_GIO_UNIX
static gboolean
xb_tool_sigint_cb(gpointer user_data)
{
	XbToolPrivate *priv = (XbToolPrivate *)user_data;
	g_debug("Handling SIGINT");
	g_cancellable_cancel(priv->cancellable);
	return FALSE;
}
#endif

int
main(int argc, char *argv[])
{
	gboolean ret;
	gboolean verbose = FALSE;
	g_autofree gchar *cmd_descriptions = NULL;
	g_autoptr(XbToolPrivate) priv = g_new0(XbToolPrivate, 1);
	g_autoptr(GError) error = NULL;
	g_autoptr(GOptionContext) context = NULL;
	const GOptionEntry options[] = {{"verbose",
					 'v',
					 0,
					 G_OPTION_ARG_NONE,
					 &verbose,
					 "Print verbose debug statements",
					 NULL},
					{"force",
					 'v',
					 0,
					 G_OPTION_ARG_NONE,
					 &priv->force,
					 "Force parsing of invalid files",
					 NULL},
					{"wait",
					 'w',
					 0,
					 G_OPTION_ARG_NONE,
					 &priv->wait,
					 "Return only when the silo is no longer valid",
					 NULL},
					{"profile",
					 'p',
					 0,
					 G_OPTION_ARG_NONE,
					 &priv->profile,
					 "Show profiling information",
					 NULL},
					{"tokenize",
					 't',
					 0,
					 G_OPTION_ARG_STRING_ARRAY,
					 &priv->tokenize,
					 "Tokenize elements for faster search, e.g. name,summary",
					 NULL},
					{NULL}};

	setlocale(LC_ALL, "");

	/* do not let GIO start a session bus */
	g_setenv("GIO_USE_VFS", "local", 1);

	/* add commands */
	priv->cmd_array = g_ptr_array_new_with_free_func((GDestroyNotify)xb_tool_item_free);
	xb_tool_add(priv->cmd_array,
		    "dump",
		    "XMLBFILE",
		    /* TRANSLATORS: command description */
		    "Dumps a XMLb file",
		    xb_tool_dump);
	xb_tool_add(priv->cmd_array,
		    "export",
		    "XMLFILE",
		    /* TRANSLATORS: command description */
		    "Exports a XMLb file",
		    xb_tool_export);
	xb_tool_add(priv->cmd_array,
		    "query",
		    "XMLBFILE XPATH [LIMIT]",
		    /* TRANSLATORS: command description */
		    "Queries a XMLb file",
		    xb_tool_query);
	xb_tool_add(priv->cmd_array,
		    "query-file",
		    "XMLBFILE [FILE] [FILE]",
		    /* TRANSLATORS: command description */
		    "Queries a XMLb file using an external XPath query",
		    xb_tool_query_file);
	xb_tool_add(priv->cmd_array,
		    "compile",
		    "XMLBFILE XMLFILE [XMLFILE]",
		    /* TRANSLATORS: command description */
		    "Compile XML to XMLb",
		    xb_tool_compile);

	/* do stuff on ctrl+c */
	priv->loop = g_main_loop_new(NULL, FALSE);
	priv->cancellable = g_cancellable_new();
	g_signal_connect(priv->cancellable, "cancelled", G_CALLBACK(xb_tool_cancelled_cb), priv);
#ifdef HAVE_GIO_UNIX
	g_unix_signal_add_full(G_PRIORITY_DEFAULT, SIGINT, xb_tool_sigint_cb, priv, NULL);
#endif

	/* sort by command name */
	g_ptr_array_sort(priv->cmd_array, (GCompareFunc)xb_tool_sort_command_name_cb);

	/* get a list of the commands */
	context = g_option_context_new(NULL);
	cmd_descriptions = xb_tool_get_descriptions(priv->cmd_array);
	g_option_context_set_summary(context, cmd_descriptions);

	/* TRANSLATORS: DFU stands for device firmware update */
	g_set_application_name("Binary XML Utility");
	g_option_context_add_main_entries(context, options, NULL);
	ret = g_option_context_parse(context, &argc, &argv, &error);
	if (!ret) {
		g_print("%s: %s\n", "Failed to parse arguments", error->message);
		return EXIT_FAILURE;
	}

	/* set verbose? */
	if (verbose)
		g_setenv("G_MESSAGES_DEBUG", "all", FALSE);

	/* run the specified command */
	ret = xb_tool_run(priv, argv[1], (gchar **)&argv[2], &error);
	if (!ret) {
		if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_FAILED)) {
			g_autofree gchar *tmp = NULL;
			tmp = g_option_context_get_help(context, TRUE, NULL);
			g_print("%s\n\n%s", error->message, tmp);
		} else {
			g_print("%s\n", error->message);
		}
		return EXIT_FAILURE;
	}

	/* success/ */
	return EXIT_SUCCESS;
}
